package org.box.metadate.cli;

import java.lang.reflect.Field;

import org.box.metadate.cli.annotation.Argument;
import org.box.metadate.cli.annotation.Arguments;
import org.box.metadate.cli.annotation.Configuration;
import org.box.metadate.cli.annotation.Option;

/**
 * 
 * Annotation's based {@link CommandLineParser} implementation for Application
 * Command Line Interface. MetaCommandLineParser uses meta information
 * represented via java annotations in an application's configuration class.
 * 
 * Simple application's configuration class can be demonstrated by the code
 * snippet below:
 * 
 * <pre>
 * &#064;Configuration()
 * public class AppConfiguration {
 * 
 * 	&#064;Option(description = &quot;Simple private yes/no option without agruments&quot;, fullName = &quot;key1&quot;, shortName = &quot;k1&quot;)
 * 	private boolean key1;
 * 
 * 	&#064;Option(description = &quot;Simple key/value option with agruments&quot;, fullName = &quot;keyValue1&quot;, shortName = &quot;kv1&quot;, hasArguments = true)
 * 	public String keyValue1;
 * 
 * 	&#064;Argument(index = 1, name = &quot;infile&quot;, required = true)
 * 	public String infile;
 * 
 * }
 * </pre>
 * 
 * Parser analyzes User's input and creates a new AppConfiguration instance with
 * injected values into corresponding fields.
 * 
 * <pre>
 * CommandLineParser&lt;AppConfiguration&gt; cliParser = new MetaCommandLineParser&lt;AppConfiguration&gt;(
 * 		AppConfiguration.class);
 * 
 * AppConfiguration cfg = cliParser.parse(args);
 * </pre>
 * 
 * @author <a href="mailto:alexander.box@gmail.com">Alexander Korobka</a>
 * 
 */
public class MetaCommandLineParser<T> implements CommandLineParser<T> {

	/**
	 * Enum of all out of box parser supported in the Apache's commons-cli
	 * library.
	 * 
	 */
	public enum ParserType {
		BasicParser(new org.apache.commons.cli.BasicParser()), GnuParser(
				new org.apache.commons.cli.GnuParser()), PosixParser(
				new org.apache.commons.cli.PosixParser());

		private final org.apache.commons.cli.Parser parser;

		private ParserType(org.apache.commons.cli.Parser parser) {
			this.parser = parser;
		}
	}

	private final Class<T> cfgClass;

	private final org.apache.commons.cli.CommandLineParser delegate;

	/**
	 * Constructors a new instance of the GnuParser.
	 * 
	 * @param cfgClass
	 *            - a class of the Application's configuration
	 */
	public MetaCommandLineParser(Class<T> cfgClass) {
		this(cfgClass, ParserType.GnuParser);
	}

	/**
	 * Constructors a new instance of the parser with the given type.
	 * 
	 * @param cfgClass
	 *            - a class of the Application's configuration
	 * @param parserType
	 *            - one of the following {@link ParserType}
	 */
	public MetaCommandLineParser(Class<T> cfgClass, ParserType parserType) {
		this.cfgClass = cfgClass;
		delegate = parserType.parser;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.box.metadate.cli.CommandLineParser#parse(java.lang.String[])
	 */
	public T parse(String[] args) throws ParseException, ValidationException {

		org.apache.commons.cli.CommandLine cmd;
		try {
			cmd = delegate.parse(createParserOptions(), args);
		} catch (org.apache.commons.cli.ParseException e) {
			throw new ParseException(e.getMessage());
		}

		validateArguments(cfgClass, cmd);

		try {
			T cfg = cfgClass.newInstance();
			injectValues(cfg, cmd);

			return cfg;
		} catch (InstantiationException e) {
			new ValidationException(e.getMessage());
		} catch (IllegalAccessException e) {
			new ValidationException(e.getMessage());
		}

		return null;
	}

	/**
	 * Injects Command Line options and arguments into the Application's
	 * configuration object
	 * 
	 * @param cfg
	 *            - the instance of Application's configuration
	 * @param cmd
	 *            - CommandLine from delegated parser
	 * 
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	protected void injectValues(T cfg, org.apache.commons.cli.CommandLine cmd)
			throws IllegalArgumentException, IllegalAccessException {

		for (Field f : cfgClass.getDeclaredFields()) {
			// populate options
			if (f.isAnnotationPresent(Option.class)) {
				Option o = f.getAnnotation(Option.class);
				if (cmd.hasOption(o.shortName())) {
					f.setAccessible(true);
					if (o.hasArguments()) {
						f.set(cfg, cmd.getOptionValue(o.shortName()));
					} else {
						f.setBoolean(cfg, true);
					}
				}
			}

			// populate enumerated arguments
			String[] args = cmd.getArgs();
			if (f.isAnnotationPresent(Argument.class)) {
				Argument a = f.getAnnotation(Argument.class);
				if (a.index() <= args.length) {
					f.setAccessible(true);
					f.set(cfg, args[a.index() - 1]);
				}
			}

			// non-enumerated args
			if (f.isAnnotationPresent(Arguments.class)) {
				f.setAccessible(true);
				f.set(cfg, args);
			}
		}
	}

	/**
	 * This is bran new (not supported in commons-cli) validation of CLI
	 * arguments annotated via {@link Argument} and {@link Arguments}
	 * annotations.
	 * 
	 * @param cmd
	 *            - a result of delegated parser processing
	 * 
	 * @throws ParseException
	 */
	protected void validateArguments(Class<?> clazz,
			org.apache.commons.cli.CommandLine cmd) throws ParseException {

		String[] args = cmd.getArgs();
		for (Field f : clazz.getDeclaredFields()) {
			// check enumerated arguments
			if (f.isAnnotationPresent(Argument.class)) {
				Argument arg = f.getAnnotation(Argument.class);
				if (arg.required() && arg.index() > args.length) {
					throw new ParseException(String.format(
							"Argument '%s' is not specified", arg.name()));
				}
			}
		}
	}

	/**
	 * Creates a Options list for the delegated parser
	 * 
	 * @return new instance of application's configuration
	 * 
	 * @throws ParseException
	 * @throws ValidationException
	 *             if the given class is not compatible or valid as
	 *             Application's configuration
	 */
	protected org.apache.commons.cli.Options createParserOptions()
			throws ParseException, ValidationException {
		return createParserOptionsInternal(cfgClass);
	}

	/**
	 * Creates a Options list for the delegated parser
	 * 
	 * @param cfgClazz
	 *            - the class of application's configuration
	 * @return new instance of application's configuration
	 * 
	 * @throws ValidationException
	 *             if the given class is not compatible or valid as an
	 *             Application's configuration
	 */
	static org.apache.commons.cli.Options createParserOptionsInternal(
			Class<?> cfgClazz) throws ValidationException {

		if (!cfgClazz.isAnnotationPresent(Configuration.class)) {
			throw new ValidationException(
					"Invalid configuration class. Configuration class must heve 'Configuration' annotation");
		}

		org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();

		for (Field f : cfgClazz.getDeclaredFields()) {
			// add options
			if (f.isAnnotationPresent(Option.class)) {
				Option o = f.getAnnotation(Option.class);

				if (!o.hasArguments()
						&& f.getType().isInstance(new Boolean(false))) {
					throw new ValidationException(
							String.format(
									"Field '%s' must have Boolean type since it has no arguments",
									f.getName()));
				}

				options.addOption(new org.apache.commons.cli.Option(o
						.shortName(), o.fullName(), o.hasArguments(), o
						.description()));
			}
		}

		return options;
	}

}
